mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-19 12:47:25 +08:00
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
This commit is contained in:
@@ -1171,8 +1171,8 @@
|
|||||||
w-full max-w-md mx-4">
|
w-full max-w-md mx-4">
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<div class="flex items-center gap-3 mb-5">
|
<div class="flex items-center gap-3 mb-5">
|
||||||
<div class="w-10 h-10 rounded-xl bg-violet-50 dark:bg-violet-900/20 flex items-center justify-center flex-shrink-0">
|
<div class="w-10 h-10 rounded-xl bg-primary-50 dark:bg-primary-900/20 flex items-center justify-center flex-shrink-0">
|
||||||
<i class="fas fa-sliders text-violet-500"></i>
|
<i class="fas fa-sliders text-primary-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<h3 id="custom-provider-modal-title" class="font-semibold text-slate-800 dark:text-slate-100 text-base"></h3>
|
<h3 id="custom-provider-modal-title" class="font-semibold text-slate-800 dark:text-slate-100 text-base"></h3>
|
||||||
@@ -1185,15 +1185,14 @@
|
|||||||
<input id="custom-provider-name" type="text" autocomplete="off" data-1p-ignore data-lpignore="true"
|
<input id="custom-provider-name" type="text" autocomplete="off" data-1p-ignore data-lpignore="true"
|
||||||
class="w-full px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-600
|
class="w-full px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-600
|
||||||
bg-slate-50 dark:bg-white/5 text-sm text-slate-800 dark:text-slate-100
|
bg-slate-50 dark:bg-white/5 text-sm text-slate-800 dark:text-slate-100
|
||||||
focus:outline-none focus:border-violet-500 transition-colors"
|
focus:outline-none focus:border-primary-500 transition-colors">
|
||||||
placeholder="siliconflow">
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5">API Base</label>
|
<label class="block text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5">API Base</label>
|
||||||
<input id="custom-provider-base" type="text"
|
<input id="custom-provider-base" type="text"
|
||||||
class="w-full px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-600
|
class="w-full px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-600
|
||||||
bg-slate-50 dark:bg-white/5 text-sm text-slate-800 dark:text-slate-100
|
bg-slate-50 dark:bg-white/5 text-sm text-slate-800 dark:text-slate-100
|
||||||
focus:outline-none focus:border-violet-500 font-mono transition-colors"
|
focus:outline-none focus:border-primary-500 font-mono transition-colors"
|
||||||
placeholder="https://...../v1">
|
placeholder="https://...../v1">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -1201,20 +1200,15 @@
|
|||||||
<input id="custom-provider-key" type="text" autocomplete="off" data-1p-ignore data-lpignore="true"
|
<input id="custom-provider-key" type="text" autocomplete="off" data-1p-ignore data-lpignore="true"
|
||||||
class="w-full px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-600
|
class="w-full px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-600
|
||||||
bg-slate-50 dark:bg-white/5 text-sm text-slate-800 dark:text-slate-100
|
bg-slate-50 dark:bg-white/5 text-sm text-slate-800 dark:text-slate-100
|
||||||
focus:outline-none focus:border-violet-500 font-mono transition-colors"
|
focus:outline-none focus:border-primary-500 font-mono transition-colors">
|
||||||
placeholder="sk-...">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5" data-i18n="models_custom_default_model">默认模型(可选)</label>
|
|
||||||
<input id="custom-provider-model" type="text"
|
|
||||||
class="w-full px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-600
|
|
||||||
bg-slate-50 dark:bg-white/5 text-sm text-slate-800 dark:text-slate-100
|
|
||||||
focus:outline-none focus:border-violet-500 font-mono transition-colors"
|
|
||||||
placeholder="deepseek-ai/DeepSeek-V3">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-slate-100 dark:border-white/5 rounded-b-2xl">
|
<div class="flex items-center justify-between gap-3 px-6 py-4 border-t border-slate-100 dark:border-white/5 rounded-b-2xl">
|
||||||
|
<button id="custom-provider-modal-delete"
|
||||||
|
class="px-3 py-2 rounded-lg text-sm font-medium text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20
|
||||||
|
cursor-pointer transition-colors duration-150 hidden"
|
||||||
|
data-i18n="models_custom_delete">删除</button>
|
||||||
<span id="custom-provider-modal-status"
|
<span id="custom-provider-modal-status"
|
||||||
class="flex-1 text-xs text-primary-500 opacity-0 transition-opacity duration-300 text-left"></span>
|
class="flex-1 text-xs text-primary-500 opacity-0 transition-opacity duration-300 text-left"></span>
|
||||||
<button id="custom-provider-modal-cancel"
|
<button id="custom-provider-modal-cancel"
|
||||||
@@ -1224,7 +1218,7 @@
|
|||||||
cursor-pointer transition-colors duration-150"
|
cursor-pointer transition-colors duration-150"
|
||||||
data-i18n="cancel">取消</button>
|
data-i18n="cancel">取消</button>
|
||||||
<button id="custom-provider-modal-save"
|
<button id="custom-provider-modal-save"
|
||||||
class="px-4 py-2 rounded-lg bg-violet-500 hover:bg-violet-600 text-white text-sm font-medium
|
class="px-4 py-2 rounded-lg bg-primary-500 hover:bg-primary-600 text-white text-sm font-medium
|
||||||
cursor-pointer transition-colors duration-150 disabled:opacity-50 disabled:cursor-not-allowed"
|
cursor-pointer transition-colors duration-150 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
data-i18n="save">保存</button>
|
data-i18n="save">保存</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -854,6 +854,14 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
flex-shrink: 0;
|
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 */
|
||||||
#chat-input {
|
#chat-input {
|
||||||
|
|||||||
@@ -32,21 +32,13 @@ const I18N = {
|
|||||||
models_clear_credential: '清除凭据',
|
models_clear_credential: '清除凭据',
|
||||||
models_base_default_hint: '留空将使用官方默认地址',
|
models_base_default_hint: '留空将使用官方默认地址',
|
||||||
models_base_default: '默认',
|
models_base_default: '默认',
|
||||||
models_custom_section: '自定义厂商',
|
models_custom_vendor_label: '自定义',
|
||||||
models_custom_section_desc: '配置多个 OpenAI 兼容厂商,自由切换',
|
|
||||||
models_custom_add: '添加自定义厂商',
|
|
||||||
models_custom_name: '名称',
|
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: '删除',
|
||||||
models_custom_delete_confirm_title: '删除自定义厂商',
|
models_custom_delete_confirm_title: '删除自定义厂商',
|
||||||
models_custom_delete_confirm_msg: '确定删除该自定义厂商吗?此操作无法撤销。',
|
models_custom_delete_confirm_msg: '确定删除该自定义厂商吗?此操作无法撤销。',
|
||||||
models_custom_name_required: '请填写名称',
|
models_custom_name_required: '请填写名称',
|
||||||
models_custom_base_required: '请填写 API Base',
|
models_custom_base_required: '请填写 API Base',
|
||||||
models_custom_empty: '尚未配置自定义厂商,点击添加',
|
|
||||||
models_custom_edit_title: '编辑自定义厂商',
|
models_custom_edit_title: '编辑自定义厂商',
|
||||||
models_custom_add_title: '添加自定义厂商',
|
models_custom_add_title: '添加自定义厂商',
|
||||||
models_capability_chat: '主模型',
|
models_capability_chat: '主模型',
|
||||||
@@ -240,10 +232,10 @@ const I18N = {
|
|||||||
menu_logs: 'Logs',
|
menu_logs: 'Logs',
|
||||||
models_title: 'Models',
|
models_title: 'Models',
|
||||||
models_desc: 'Manage chat, image, voice, embedding and search capabilities in one place',
|
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_vendors_desc: 'Configured once, shared by multiple model capabilities',
|
||||||
models_section_capabilities: 'Capabilities',
|
models_section_capabilities: 'Capabilities',
|
||||||
models_add_vendor: 'Add Vendor',
|
models_add_vendor: 'Add Provider',
|
||||||
models_provider: 'Provider',
|
models_provider: 'Provider',
|
||||||
models_model: 'Model',
|
models_model: 'Model',
|
||||||
models_voice: 'Voice',
|
models_voice: 'Voice',
|
||||||
@@ -253,21 +245,13 @@ const I18N = {
|
|||||||
models_clear_credential: 'Clear credentials',
|
models_clear_credential: 'Clear credentials',
|
||||||
models_base_default_hint: 'Leave blank to use the official default base URL',
|
models_base_default_hint: 'Leave blank to use the official default base URL',
|
||||||
models_base_default: 'Default',
|
models_base_default: 'Default',
|
||||||
models_custom_section: 'Custom Providers',
|
models_custom_vendor_label: 'Custom',
|
||||||
models_custom_section_desc: 'Configure multiple OpenAI-compatible providers and switch freely',
|
|
||||||
models_custom_add: 'Add custom provider',
|
|
||||||
models_custom_name: 'Name',
|
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: 'Delete',
|
||||||
models_custom_delete_confirm_title: 'Delete custom provider',
|
models_custom_delete_confirm_title: 'Delete custom provider',
|
||||||
models_custom_delete_confirm_msg: 'Delete this custom provider? This cannot be undone.',
|
models_custom_delete_confirm_msg: 'Delete this custom provider? This cannot be undone.',
|
||||||
models_custom_name_required: 'Name is required',
|
models_custom_name_required: 'Name is required',
|
||||||
models_custom_base_required: 'API Base 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_edit_title: 'Edit custom provider',
|
||||||
models_custom_add_title: 'Add custom provider',
|
models_custom_add_title: 'Add custom provider',
|
||||||
models_capability_chat: 'Main Model',
|
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_msg: 'Send /memory rebuild-index in the chat to rebuild the index.',
|
||||||
models_embedding_saved_ok: 'Go',
|
models_embedding_saved_ok: 'Go',
|
||||||
models_pick_provider: 'Pick a provider',
|
models_pick_provider: 'Pick a provider',
|
||||||
models_clear_confirm_title: 'Clear vendor credentials',
|
models_clear_confirm_title: 'Clear provider credentials',
|
||||||
models_clear_confirm_msg: 'Remove this vendor\'s API Key and Base URL? Capabilities relying on it will stop working.',
|
models_clear_confirm_msg: 'Remove this provider\'s API Key and Base URL? Capabilities relying on it will stop working.',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
ok: 'OK',
|
ok: 'OK',
|
||||||
@@ -4766,12 +4750,12 @@ function renderModelsView() {
|
|||||||
const container = document.getElementById('models-content');
|
const container = document.getElementById('models-content');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
container.appendChild(renderVendorsSection());
|
container.appendChild(renderVendorsSection());
|
||||||
container.appendChild(renderCustomProvidersSection());
|
|
||||||
MODELS_CAPABILITY_DEFS.forEach(def => container.appendChild(renderCapabilityCard(def)));
|
MODELS_CAPABILITY_DEFS.forEach(def => container.appendChild(renderCapabilityCard(def)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// True when a provider card is one of the expanded custom (OpenAI-compatible)
|
// 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:<id>") — shown in the vendor grid alongside built-in
|
||||||
|
// vendors, but edited via the dedicated custom-provider modal.
|
||||||
function isCustomProviderCard(p) {
|
function isCustomProviderCard(p) {
|
||||||
return !!(p && p.is_custom && p.custom_name);
|
return !!(p && p.is_custom && p.custom_name);
|
||||||
}
|
}
|
||||||
@@ -4782,10 +4766,9 @@ function renderVendorsSection() {
|
|||||||
const wrap = document.createElement('div');
|
const wrap = document.createElement('div');
|
||||||
wrap.className = 'bg-white dark:bg-[#1A1A1A] rounded-xl border border-slate-200 dark:border-white/10 p-6';
|
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
|
// Custom providers always show once created (even without an api key,
|
||||||
// vendor grid focused on the canonical (field-based) providers.
|
// e.g. a local vLLM/Ollama endpoint); built-in vendors show when configured.
|
||||||
const builtinProviders = modelsState.providers.filter(p => !isCustomProviderCard(p));
|
const configured = modelsState.providers.filter(p => p.configured || isCustomProviderCard(p));
|
||||||
const configured = builtinProviders.filter(p => p.configured);
|
|
||||||
|
|
||||||
const header = `
|
const header = `
|
||||||
<div class="flex items-start gap-3 mb-5">
|
<div class="flex items-start gap-3 mb-5">
|
||||||
@@ -4796,7 +4779,6 @@ function renderVendorsSection() {
|
|||||||
<h3 class="font-semibold text-slate-800 dark:text-slate-100">${t('models_section_vendors')}</h3>
|
<h3 class="font-semibold text-slate-800 dark:text-slate-100">${t('models_section_vendors')}</h3>
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">${t('models_section_vendors_desc')}</p>
|
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">${t('models_section_vendors_desc')}</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-xs text-slate-400 dark:text-slate-500 mt-2 flex-shrink-0">${configured.length}/${builtinProviders.length}</span>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
let body;
|
let body;
|
||||||
@@ -4822,8 +4804,13 @@ function renderVendorsSection() {
|
|||||||
function renderVendorChip(p) {
|
function renderVendorChip(p) {
|
||||||
// The masked API key is intentionally not surfaced here; it is shown
|
// The masked API key is intentionally not surfaced here; it is shown
|
||||||
// inside the edit modal so the chip stays uncluttered and scannable.
|
// 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 `
|
return `
|
||||||
<button onclick="openVendorModal('${escapeHtml(p.id)}')"
|
<button onclick="${onclick}"
|
||||||
class="group flex items-center gap-3 px-3 py-2.5 rounded-lg border border-slate-200 dark:border-white/10
|
class="group flex items-center gap-3 px-3 py-2.5 rounded-lg border border-slate-200 dark:border-white/10
|
||||||
bg-slate-50 dark:bg-white/5 hover:border-primary-300 dark:hover:border-primary-500/50
|
bg-slate-50 dark:bg-white/5 hover:border-primary-300 dark:hover:border-primary-500/50
|
||||||
cursor-pointer transition-colors duration-150 text-left">
|
cursor-pointer transition-colors duration-150 text-left">
|
||||||
@@ -4856,112 +4843,10 @@ function renderProviderLogo(p, sizePx) {
|
|||||||
</span>`;
|
</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Custom providers section (multiple OpenAI-compatible) -------
|
|
||||||
// Renders the user-defined OpenAI-compatible providers as a dedicated,
|
|
||||||
// independently managed list: add / edit / delete / activate. The backend
|
|
||||||
// expands `custom_providers` into provider cards with id="custom:<id>",
|
|
||||||
// is_custom=true, custom_id, custom_name and an `active` flag (see
|
|
||||||
// ModelsHandler._custom_provider_cards / _provider_overview).
|
|
||||||
// All button interactions use data-* attributes + event delegation (no inline
|
|
||||||
// onclick) to avoid XSS via user-supplied names.
|
|
||||||
|
|
||||||
function getCustomProviderCards() {
|
function getCustomProviderCards() {
|
||||||
return modelsState.providers.filter(isCustomProviderCard);
|
return modelsState.providers.filter(isCustomProviderCard);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCustomProvidersSection() {
|
|
||||||
const wrap = document.createElement('div');
|
|
||||||
wrap.className = 'bg-white dark:bg-[#1A1A1A] rounded-xl border border-slate-200 dark:border-white/10 p-6';
|
|
||||||
|
|
||||||
const customs = getCustomProviderCards();
|
|
||||||
|
|
||||||
const header = `
|
|
||||||
<div class="flex items-start gap-3 mb-5">
|
|
||||||
<div class="w-9 h-9 rounded-lg bg-violet-50 dark:bg-violet-900/30 flex items-center justify-center flex-shrink-0">
|
|
||||||
<i class="fas fa-sliders text-violet-500 text-sm"></i>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<h3 class="font-semibold text-slate-800 dark:text-slate-100">${t('models_custom_section')}</h3>
|
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">${t('models_custom_section_desc')}</p>
|
|
||||||
</div>
|
|
||||||
<button data-action="add-custom"
|
|
||||||
class="mt-1 px-3 py-1.5 rounded-lg text-xs font-medium bg-violet-50 dark:bg-violet-900/30 text-violet-600 dark:text-violet-400 hover:bg-violet-100 dark:hover:bg-violet-900/50 cursor-pointer transition-colors flex-shrink-0">
|
|
||||||
<i class="fas fa-plus text-[10px] mr-1"></i>${t('models_custom_add')}
|
|
||||||
</button>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
let body;
|
|
||||||
if (customs.length === 0) {
|
|
||||||
body = `
|
|
||||||
<div class="flex flex-col items-center justify-center py-8 px-4 rounded-lg border border-dashed border-slate-200 dark:border-white/10">
|
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400 text-center">${t('models_custom_empty')}</p>
|
|
||||||
</div>`;
|
|
||||||
} else {
|
|
||||||
body = `<div class="space-y-2.5">
|
|
||||||
${customs.map(renderCustomProviderRow).join('')}
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
? `<span class="px-2 py-0.5 rounded-full text-[10px] font-medium bg-emerald-50 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400 flex-shrink-0">
|
|
||||||
<i class="fas fa-check text-[9px] mr-0.5"></i>${t('models_custom_active')}</span>`
|
|
||||||
: `<button data-action="set-active-custom" data-provider-id="${escapeHtml(id)}"
|
|
||||||
class="px-2 py-0.5 rounded-full text-[10px] font-medium border border-slate-200 dark:border-white/10 text-slate-500 dark:text-slate-400 hover:border-emerald-300 hover:text-emerald-500 cursor-pointer transition-colors flex-shrink-0">
|
|
||||||
${t('models_custom_set_active')}</button>`;
|
|
||||||
|
|
||||||
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
|
|
||||||
? `<span class="text-[11px] text-slate-400 dark:text-slate-500 font-mono truncate">${escapeHtml(p.model)}</span>`
|
|
||||||
: '';
|
|
||||||
const base = p.api_base
|
|
||||||
? `<span class="text-[11px] text-slate-400 dark:text-slate-500 font-mono truncate">${escapeHtml(p.api_base)}</span>`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="flex items-center gap-3 px-3 py-2.5 rounded-lg border ${ring} transition-colors" data-provider-id="${escapeHtml(id)}">
|
|
||||||
${renderProviderLogo(p, 28)}
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-sm font-medium text-slate-800 dark:text-slate-100 truncate">${nameEsc}</span>
|
|
||||||
${activeBadge}
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 mt-0.5">${base}${model ? '<span class="text-slate-300 dark:text-slate-600">·</span>' + model : ''}</div>
|
|
||||||
</div>
|
|
||||||
<button data-action="edit-custom" data-provider-id="${escapeHtml(id)}" title="${t('models_custom_edit_title')}"
|
|
||||||
class="w-8 h-8 rounded-lg flex items-center justify-center text-slate-400 dark:text-slate-500 hover:text-primary-500 hover:bg-slate-100 dark:hover:bg-white/10 cursor-pointer transition-colors flex-shrink-0">
|
|
||||||
<i class="fas fa-pen-to-square text-[12px]"></i>
|
|
||||||
</button>
|
|
||||||
<button data-action="delete-custom" data-provider-id="${escapeHtml(id)}" title="${t('models_custom_delete')}"
|
|
||||||
class="w-8 h-8 rounded-lg flex items-center justify-center text-slate-400 dark:text-slate-500 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 cursor-pointer transition-colors flex-shrink-0">
|
|
||||||
<i class="fas fa-trash-can text-[12px]"></i>
|
|
||||||
</button>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Capability cards (Layer 2) ---------------------------------
|
// ---------- Capability cards (Layer 2) ---------------------------------
|
||||||
|
|
||||||
function renderCapabilityCard(def) {
|
function renderCapabilityCard(def) {
|
||||||
@@ -5776,6 +5661,14 @@ function decorateVendorModalPicker(ddEl, opts) {
|
|||||||
// Tag the row so the global active-row ✓ rule is suppressed in CSS
|
// Tag the row so the global active-row ✓ rule is suppressed in CSS
|
||||||
// (otherwise configured AND selected rows would render two checks).
|
// (otherwise configured AND selected rows would render two checks).
|
||||||
item.classList.add('vendor-picker-item');
|
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;
|
if (!opt._configured) return;
|
||||||
const check = document.createElement('i');
|
const check = document.createElement('i');
|
||||||
check.className = 'fas fa-check vendor-picker-configured-mark';
|
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
|
// currently selected vendor via its own background highlight, so we
|
||||||
// intentionally suppress the global active-row ✓ for this picker
|
// intentionally suppress the global active-row ✓ for this picker
|
||||||
// (see CSS) — otherwise configured + selected rows would show two.
|
// (see CSS) — otherwise configured + selected rows would show two.
|
||||||
// Custom (OpenAI-compatible) providers are managed in their own
|
// Expanded custom provider cards ("custom:<id>") are edited via their
|
||||||
// section with a dedicated modal, so they are excluded from this
|
// dedicated modal, so they are excluded from this picker. Picking the
|
||||||
// built-in vendor picker.
|
// "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 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 => ({
|
const pickerOpts = builtinProviders.map(p => ({
|
||||||
value: p.id,
|
value: p.id,
|
||||||
label: localizedLabel(p.label),
|
label: localizedLabel(p.label),
|
||||||
_configured: !!p.configured,
|
_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);
|
decorateVendorModalPicker(pickerEl, pickerOpts);
|
||||||
fillVendorModalForProvider(defaultId);
|
onPick(defaultId);
|
||||||
} else {
|
} else {
|
||||||
pickerWrap.classList.add('hidden');
|
pickerWrap.classList.add('hidden');
|
||||||
fillVendorModalForProvider(providerId);
|
fillVendorModalForProvider(providerId);
|
||||||
@@ -6276,11 +6189,9 @@ function openCustomProviderModal(providerId) {
|
|||||||
const nameInput = document.getElementById('custom-provider-name');
|
const nameInput = document.getElementById('custom-provider-name');
|
||||||
const baseInput = document.getElementById('custom-provider-base');
|
const baseInput = document.getElementById('custom-provider-base');
|
||||||
const keyInput = document.getElementById('custom-provider-key');
|
const keyInput = document.getElementById('custom-provider-key');
|
||||||
const modelInput = document.getElementById('custom-provider-model');
|
|
||||||
|
|
||||||
nameInput.value = card ? (card.custom_name || '') : '';
|
nameInput.value = card ? (card.custom_name || '') : '';
|
||||||
baseInput.value = card ? (card.api_base || '') : '';
|
baseInput.value = card ? (card.api_base || '') : '';
|
||||||
modelInput.value = card ? (card.model || '') : '';
|
|
||||||
|
|
||||||
// Surface the masked key as the value for configured providers so the
|
// Surface the masked key as the value for configured providers so the
|
||||||
// "already set" state is unambiguous; an untouched masked value means
|
// "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-cancel').onclick = closeCustomProviderModal;
|
||||||
document.getElementById('custom-provider-modal-save').onclick = saveCustomProviderModal;
|
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) {
|
function onOverlayClick(e) {
|
||||||
if (e.target === overlay) {
|
if (e.target === overlay) {
|
||||||
closeCustomProviderModal();
|
closeCustomProviderModal();
|
||||||
@@ -6325,7 +6243,6 @@ function closeCustomProviderModal() {
|
|||||||
function saveCustomProviderModal() {
|
function saveCustomProviderModal() {
|
||||||
const name = document.getElementById('custom-provider-name').value.trim();
|
const name = document.getElementById('custom-provider-name').value.trim();
|
||||||
const apiBase = document.getElementById('custom-provider-base').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');
|
const keyInput = document.getElementById('custom-provider-key');
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
@@ -6350,7 +6267,6 @@ function saveCustomProviderModal() {
|
|||||||
action: 'set_custom_provider',
|
action: 'set_custom_provider',
|
||||||
name: name,
|
name: name,
|
||||||
api_base: apiBase,
|
api_base: apiBase,
|
||||||
model: model,
|
|
||||||
};
|
};
|
||||||
if (apiKey) payload.api_key = apiKey;
|
if (apiKey) payload.api_key = apiKey;
|
||||||
if (editing) payload.id = customProviderModalState.editId;
|
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) {
|
function deleteCustomProvider(providerId) {
|
||||||
showConfirmDialog({
|
showConfirmDialog({
|
||||||
title: t('models_custom_delete_confirm_title'),
|
title: t('models_custom_delete_confirm_title'),
|
||||||
@@ -6397,7 +6303,10 @@ function deleteCustomProvider(providerId) {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ action: 'delete_custom_provider', id: providerId }),
|
body: JSON.stringify({ action: 'delete_custom_provider', id: providerId }),
|
||||||
}).then(r => r.json()).then(data => {
|
}).then(r => r.json()).then(data => {
|
||||||
if (data.status === 'success') loadModelsView();
|
if (data.status === 'success') {
|
||||||
|
closeCustomProviderModal();
|
||||||
|
loadModelsView();
|
||||||
|
}
|
||||||
}).catch(() => { /* noop */ });
|
}).catch(() => { /* noop */ });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1643,6 +1643,32 @@ class ConfigHandler:
|
|||||||
"api_key_field": p.get("api_key_field"),
|
"api_key_field": p.get("api_key_field"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Expose user-defined custom providers as "custom:<id>" 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 "")
|
raw_pwd = str(local_config.get("web_password", "") or "")
|
||||||
masked_pwd = ("*" * len(raw_pwd)) if raw_pwd else ""
|
masked_pwd = ("*" * len(raw_pwd)) if raw_pwd else ""
|
||||||
|
|
||||||
@@ -2189,6 +2215,17 @@ class ModelsHandler:
|
|||||||
})
|
})
|
||||||
return cards
|
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
|
@classmethod
|
||||||
def _provider_overview(cls) -> List[dict]:
|
def _provider_overview(cls) -> List[dict]:
|
||||||
"""All known providers (configured first, unconfigured after).
|
"""All known providers (configured first, unconfigured after).
|
||||||
@@ -2202,13 +2239,18 @@ class ModelsHandler:
|
|||||||
"""
|
"""
|
||||||
local_config = conf()
|
local_config = conf()
|
||||||
custom_cards = cls._custom_provider_cards(local_config)
|
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 = []
|
items = []
|
||||||
for pid, p in ConfigHandler.PROVIDER_MODELS.items():
|
for pid, p in ConfigHandler.PROVIDER_MODELS.items():
|
||||||
if pid == "custom" and custom_cards:
|
if pid == "custom" and custom_cards:
|
||||||
# Multi-provider mode: emit the expanded cards instead of the
|
# Multi-provider mode: emit the expanded cards, plus the
|
||||||
# single legacy custom card.
|
# legacy card when it is still in use.
|
||||||
items.extend(custom_cards)
|
items.extend(custom_cards)
|
||||||
continue
|
if not keep_legacy_custom:
|
||||||
|
continue
|
||||||
key_field = p.get("api_key_field")
|
key_field = p.get("api_key_field")
|
||||||
base_field = p.get("api_base_key")
|
base_field = p.get("api_base_key")
|
||||||
raw_key = local_config.get(key_field, "") if key_field else ""
|
raw_key = local_config.get(key_field, "") if key_field else ""
|
||||||
@@ -2253,11 +2295,15 @@ class ModelsHandler:
|
|||||||
provider_id = "linkai"
|
provider_id = "linkai"
|
||||||
# In multi-provider mode, replace the single "custom" entry with the
|
# In multi-provider mode, replace the single "custom" entry with the
|
||||||
# expanded "custom:<id>" ids so the chat dropdown matches the cards.
|
# expanded "custom:<id>" ids so the chat dropdown matches the cards.
|
||||||
|
# The legacy "custom" entry stays when its flat config is still used.
|
||||||
provider_ids = []
|
provider_ids = []
|
||||||
custom_cards = cls._custom_provider_cards(local_config)
|
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():
|
for pid in ConfigHandler.PROVIDER_MODELS.keys():
|
||||||
if pid == "custom" and custom_cards:
|
if pid == "custom" and custom_cards:
|
||||||
provider_ids.extend(c["id"] for c in custom_cards)
|
provider_ids.extend(c["id"] for c in custom_cards)
|
||||||
|
if keep_legacy_custom:
|
||||||
|
provider_ids.append(pid)
|
||||||
else:
|
else:
|
||||||
provider_ids.append(pid)
|
provider_ids.append(pid)
|
||||||
return {
|
return {
|
||||||
@@ -2881,11 +2927,14 @@ class ModelsHandler:
|
|||||||
existing["api_base"] = api_base
|
existing["api_base"] = api_base
|
||||||
if api_key:
|
if api_key:
|
||||||
existing["api_key"] = api_key
|
existing["api_key"] = api_key
|
||||||
# model is always overwritten (empty clears the default model).
|
# Only touch model when explicitly provided in the payload; an
|
||||||
if model:
|
# explicit empty string clears it, a missing key keeps it (the
|
||||||
existing["model"] = model
|
# UI modal no longer sends model, so manual config survives edits).
|
||||||
else:
|
if "model" in data:
|
||||||
existing.pop("model", None)
|
if model:
|
||||||
|
existing["model"] = model
|
||||||
|
else:
|
||||||
|
existing.pop("model", None)
|
||||||
created = False
|
created = False
|
||||||
|
|
||||||
# Decide bot_type — only switch when explicitly requested.
|
# Decide bot_type — only switch when explicitly requested.
|
||||||
|
|||||||
@@ -1,69 +1,31 @@
|
|||||||
---
|
---
|
||||||
title: Custom
|
title: カスタム
|
||||||
description: Custom vendor configuration for third-party API proxies and local models
|
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
|
- **サードパーティ API プロキシ**:統一された API アドレスで複数のモデルを呼び出す
|
||||||
- **Local models**: models deployed locally with tools like Ollama, vLLM, LocalAI
|
- **ローカルモデル**:Ollama、vLLM などのツールでローカルにデプロイしたモデル
|
||||||
- **Private deployments**: model services deployed inside an enterprise
|
- **プライベートデプロイ**:企業内部にデプロイされたモデルサービス
|
||||||
|
|
||||||
<Note>
|
## Web コンソールでの設定
|
||||||
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.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
## Text Chat
|
推奨方法です。Web コンソールの「モデル」ページで「プロバイダーを追加」をクリックし、「カスタム」を選択して、名称・API Base・API Key を入力します。複数のカスタムプロバイダーを追加でき、追加後は「メインモデル」でプロバイダーとモデルを選択すると有効になります。
|
||||||
|
|
||||||
### Third-party API proxy
|
<img width="900" src="https://cdn.jsdelivr.net/gh/zhayujie/cowagent-assets@main/screenshots/en/web-console-custom-model-config.png" />
|
||||||
|
|
||||||
```json
|
主なローカルデプロイツールのデフォルトアドレス:
|
||||||
{
|
|
||||||
"bot_type": "custom",
|
|
||||||
"model": "",
|
|
||||||
"custom_api_key": "YOUR_API_KEY",
|
|
||||||
"custom_api_base": "https://{your-proxy.com}/v1"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Parameter | Description |
|
| ツール | デフォルト API Base |
|
||||||
| --- | --- |
|
|
||||||
| `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 |
|
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [Ollama](https://ollama.com) | `http://localhost:11434/v1` |
|
| [Ollama](https://ollama.com) | `http://localhost:11434/v1` |
|
||||||
| [vLLM](https://docs.vllm.ai) | `http://localhost:8000/v1` |
|
| [vLLM](https://docs.vllm.ai) | `http://localhost:8000/v1` |
|
||||||
| [LocalAI](https://localai.io) | `http://localhost:8080/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.json` を直接編集することもできます。`custom_providers` リストに複数のプロバイダーを定義し、`bot_type` を `"custom:<id>"` に設定していずれかを有効化します:
|
||||||
|
|
||||||
```
|
|
||||||
/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:<id>"`:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -71,34 +33,30 @@ If you need to configure several OpenAI-compatible third-party services at once
|
|||||||
"custom_providers": [
|
"custom_providers": [
|
||||||
{
|
{
|
||||||
"id": "3f2a9c1b",
|
"id": "3f2a9c1b",
|
||||||
"name": "siliconflow",
|
"name": "プロバイダーA",
|
||||||
"api_key": "YOUR_SILICONFLOW_KEY",
|
"api_key": "YOUR_API_KEY_A",
|
||||||
"api_base": "https://api.siliconflow.cn/v1",
|
"api_base": "https://api.a.com/v1",
|
||||||
"model": "deepseek-ai/DeepSeek-V3"
|
"model": "deepseek-v3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "a1b2c3d4",
|
"id": "a1b2c3d4",
|
||||||
"name": "qiniu",
|
"name": "プロバイダーB",
|
||||||
"api_key": "YOUR_QINIU_KEY",
|
"api_key": "YOUR_API_KEY_B",
|
||||||
"api_base": "https://api.qnaigc.com/v1",
|
"api_base": "https://api.b.com/v1",
|
||||||
"model": "deepseek-v3"
|
"model": "qwen3-max"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| Parameter | Description |
|
| パラメータ | 説明 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `custom_providers` | List of custom providers; each item has `id`, `name`, `api_key`, `api_base`, and an optional `model` |
|
| `custom_providers` | カスタムプロバイダーのリスト。各項目は `id`、`name`、`api_base`、`api_key`(任意)、`model`(任意)を含む |
|
||||||
| `bot_type` | Set to `"custom:<id>"` to activate a specific provider; `id` is the provider's unique identifier |
|
| `bot_type` | `"custom:<id>"` に設定して対応するプロバイダーを有効化 |
|
||||||
| `id` | Server-generated short identifier (8-char hex); used as the primary key for each provider |
|
| `id` | 一意の識別子(8 桁の 16 進数)。Web コンソールから追加すると自動生成され、手動設定の場合は重複しない任意の文字列でよい |
|
||||||
| `name` | User-facing display label — can be freely renamed without breaking anything |
|
| `name` | 表示名。自由に変更可能 |
|
||||||
| `model` | Optional; when set, this model is used instead of the global `model` field |
|
| `model` | このプロバイダーで使用するモデル。有効化時に適用される |
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
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])"`).
|
従来の単一プロバイダー設定(`bot_type` を `"custom"` にし、`custom_api_key` / `custom_api_base` を使用)は引き続き互換性があり、変更なしでそのまま利用できます。
|
||||||
</Note>
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
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.
|
|
||||||
</Note>
|
</Note>
|
||||||
|
|||||||
@@ -1,51 +1,21 @@
|
|||||||
---
|
---
|
||||||
title: Custom
|
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
|
- **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
|
- **Private deployments**: model services deployed inside an enterprise
|
||||||
|
|
||||||
<Note>
|
## Web Console
|
||||||
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.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
## 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
|
<img width="900" src="https://cdn.jsdelivr.net/gh/zhayujie/cowagent-assets@main/screenshots/en/web-console-custom-model-config.png" />
|
||||||
|
|
||||||
```json
|
Default endpoints of common local deployment tools:
|
||||||
{
|
|
||||||
"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 |
|
| 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` |
|
| [vLLM](https://docs.vllm.ai) | `http://localhost:8000/v1` |
|
||||||
| [LocalAI](https://localai.io) | `http://localhost:8080/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:
|
You can also edit `config.json` directly: define multiple providers in the `custom_providers` list and set `bot_type` to `"custom:<id>"` to activate one of them:
|
||||||
|
|
||||||
```
|
|
||||||
/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:<id>"`:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -71,17 +33,17 @@ If you need to configure several OpenAI-compatible third-party services at once
|
|||||||
"custom_providers": [
|
"custom_providers": [
|
||||||
{
|
{
|
||||||
"id": "3f2a9c1b",
|
"id": "3f2a9c1b",
|
||||||
"name": "siliconflow",
|
"name": "ProviderA",
|
||||||
"api_key": "YOUR_SILICONFLOW_KEY",
|
"api_key": "YOUR_API_KEY_A",
|
||||||
"api_base": "https://api.siliconflow.cn/v1",
|
"api_base": "https://api.a.com/v1",
|
||||||
"model": "deepseek-ai/DeepSeek-V3"
|
"model": "deepseek-v3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "a1b2c3d4",
|
"id": "a1b2c3d4",
|
||||||
"name": "qiniu",
|
"name": "ProviderB",
|
||||||
"api_key": "YOUR_QINIU_KEY",
|
"api_key": "YOUR_API_KEY_B",
|
||||||
"api_base": "https://api.qnaigc.com/v1",
|
"api_base": "https://api.b.com/v1",
|
||||||
"model": "deepseek-v3"
|
"model": "qwen3-max"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -89,16 +51,12 @@ If you need to configure several OpenAI-compatible third-party services at once
|
|||||||
|
|
||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `custom_providers` | List of custom providers; each item has `id`, `name`, `api_key`, `api_base`, and an optional `model` |
|
| `custom_providers` | List of custom providers; each item has `id`, `name`, `api_base`, `api_key` (optional) and `model` (optional) |
|
||||||
| `bot_type` | Set to `"custom:<id>"` to activate a specific provider; `id` is the provider's unique identifier |
|
| `bot_type` | Set to `"custom:<id>"` to activate the corresponding provider |
|
||||||
| `id` | Server-generated short identifier (8-char hex); used as the primary key for each provider |
|
| `id` | Unique identifier (8-char hex); auto-generated when adding via the Web console, or any unique string when editing manually |
|
||||||
| `name` | User-facing display label — can be freely renamed without breaking anything |
|
| `name` | Display label, can be renamed freely |
|
||||||
| `model` | Optional; when set, this model is used instead of the global `model` field |
|
| `model` | Model used by this provider, takes effect when activated |
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
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])"`).
|
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.
|
||||||
</Note>
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
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.
|
|
||||||
</Note>
|
</Note>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: DeepSeek
|
|||||||
description: DeepSeek model configuration (Text Chat + Thinking Mode)
|
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
|
## Text Chat
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
---
|
---
|
||||||
title: Models Overview
|
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
|
## 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 | ✅ | | | | | |
|
| [DeepSeek](/models/deepseek) | deepseek-v4-flash / pro | ✅ | | | | | |
|
||||||
| [MiniMax](/models/minimax) | MiniMax-M3 | ✅ | ✅ | ✅ | | ✅ | |
|
| [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 | ✅ | ✅ | | | | |
|
| [Kimi](/models/kimi) | kimi-k2.6 | ✅ | ✅ | | | | |
|
||||||
| [ERNIE](/models/qianfan) | ernie-5.1 | ✅ | ✅ | | | | |
|
| [ERNIE](/models/qianfan) | ernie-5.1 | ✅ | ✅ | | | | |
|
||||||
| [MiMo](/models/mimo) | mimo-v2.5-pro / v2.5 | ✅ | ✅ | | | ✅ | |
|
| [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 | ✅ | | | | | |
|
| [Custom](/models/custom) | Local models / third-party proxies | ✅ | | | | | |
|
||||||
|
|
||||||
<Tip>
|
<Tip>
|
||||||
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.
|
||||||
</Tip>
|
</Tip>
|
||||||
|
|
||||||
## How to Configure
|
## How to Configure
|
||||||
@@ -35,4 +35,4 @@ A snapshot of each vendor's capabilities. "Text" refers to the main chat model;
|
|||||||
|
|
||||||
<img width="900" src="https://cdn.jsdelivr.net/gh/zhayujie/cowagent-assets@main/screenshots/en/web-console-models-config.png" />
|
<img width="900" src="https://cdn.jsdelivr.net/gh/zhayujie/cowagent-assets@main/screenshots/en/web-console-models-config.png" />
|
||||||
|
|
||||||
**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`.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: LinkAI
|
|||||||
description: Access text, vision, image, speech, and embedding capabilities through the LinkAI platform
|
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.
|
||||||
|
|
||||||
<Tip>
|
<Tip>
|
||||||
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.
|
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.
|
||||||
|
|||||||
@@ -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:
|
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 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:
|
To force a specific Vision model, set it explicitly in the configuration:
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
|
| `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_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 |
|
| `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
|
## Image Understanding
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Qwen
|
|||||||
description: Qwen model configuration (Text / Image Understanding / Image Generation / Speech-to-Text / Text-to-Speech / Embedding)
|
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`.
|
||||||
|
|
||||||
<Tip>
|
<Tip>
|
||||||
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.
|
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.
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ Related commits: [30c6d9b](https://github.com/zhayujie/CowAgent/commit/30c6d9b)
|
|||||||
|
|
||||||
## 💰 Coding Plan Support
|
## 💰 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.
|
See [Coding Plan docs](https://docs.cowagent.ai/en/models/coding-plan) for detailed configuration.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
- **Claude Opus 4.7**: Added `claude-opus-4-7` model support
|
||||||
- **GLM 5.1**: Added `glm-5.1` model support
|
- **GLM 5.1**: Added `glm-5.1` model support
|
||||||
- **Kimi Coding Plan**: Support for Kimi Coding Plan mode
|
- **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
|
## 💬 Web Console Improvements
|
||||||
|
|
||||||
|
|||||||
@@ -1,69 +1,31 @@
|
|||||||
---
|
---
|
||||||
title: Custom
|
title: 自定义
|
||||||
description: Custom vendor configuration for third-party API proxies and local models
|
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
|
- **第三方 API 代理**:通过统一的 API 地址调用多种模型
|
||||||
- **Local models**: models deployed locally with tools like Ollama, vLLM, LocalAI
|
- **本地模型**:使用 Ollama、vLLM 等工具在本地部署的模型
|
||||||
- **Private deployments**: model services deployed inside an enterprise
|
- **私有化部署**:企业内部部署的模型服务
|
||||||
|
|
||||||
<Note>
|
## Web 端配置
|
||||||
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.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
## Text Chat
|
推荐方式。在 Web 控制台「模型」页面点击「添加厂商」,选择「自定义」,填写名称、API Base 和 API Key 即可。支持添加多个自定义厂商,添加后在「主模型」中选择对应厂商和模型即可启用。
|
||||||
|
|
||||||
### Third-party API proxy
|
<img width="900" src="https://cdn.jsdelivr.net/gh/zhayujie/cowagent-assets@main/screenshots/en/web-console-custom-model-config.png" />
|
||||||
|
|
||||||
```json
|
本地部署工具的默认地址:
|
||||||
{
|
|
||||||
"bot_type": "custom",
|
|
||||||
"model": "",
|
|
||||||
"custom_api_key": "YOUR_API_KEY",
|
|
||||||
"custom_api_base": "https://{your-proxy.com}/v1"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Parameter | Description |
|
| 工具 | 默认 API Base |
|
||||||
| --- | --- |
|
|
||||||
| `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 |
|
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [Ollama](https://ollama.com) | `http://localhost:11434/v1` |
|
| [Ollama](https://ollama.com) | `http://localhost:11434/v1` |
|
||||||
| [vLLM](https://docs.vllm.ai) | `http://localhost:8000/v1` |
|
| [vLLM](https://docs.vllm.ai) | `http://localhost:8000/v1` |
|
||||||
| [LocalAI](https://localai.io) | `http://localhost:8080/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.json`,在 `custom_providers` 列表中定义多个厂商,并将 `bot_type` 设置为 `"custom:<id>"` 来激活其中一个:
|
||||||
|
|
||||||
```
|
|
||||||
/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:<id>"`:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -71,34 +33,30 @@ If you need to configure several OpenAI-compatible third-party services at once
|
|||||||
"custom_providers": [
|
"custom_providers": [
|
||||||
{
|
{
|
||||||
"id": "3f2a9c1b",
|
"id": "3f2a9c1b",
|
||||||
"name": "siliconflow",
|
"name": "厂商A",
|
||||||
"api_key": "YOUR_SILICONFLOW_KEY",
|
"api_key": "YOUR_API_KEY_A",
|
||||||
"api_base": "https://api.siliconflow.cn/v1",
|
"api_base": "https://api.a.com/v1",
|
||||||
"model": "deepseek-ai/DeepSeek-V3"
|
"model": "deepseek-v3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "a1b2c3d4",
|
"id": "a1b2c3d4",
|
||||||
"name": "qiniu",
|
"name": "厂商B",
|
||||||
"api_key": "YOUR_QINIU_KEY",
|
"api_key": "YOUR_API_KEY_B",
|
||||||
"api_base": "https://api.qnaigc.com/v1",
|
"api_base": "https://api.b.com/v1",
|
||||||
"model": "deepseek-v3"
|
"model": "qwen3-max"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| Parameter | Description |
|
| 参数 | 说明 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `custom_providers` | List of custom providers; each item has `id`, `name`, `api_key`, `api_base`, and an optional `model` |
|
| `custom_providers` | 自定义厂商列表,每项包含 `id`、`name`、`api_base`、`api_key`(可选)、`model`(可选) |
|
||||||
| `bot_type` | Set to `"custom:<id>"` to activate a specific provider; `id` is the provider's unique identifier |
|
| `bot_type` | 设置为 `"custom:<id>"` 激活对应厂商 |
|
||||||
| `id` | Server-generated short identifier (8-char hex); used as the primary key for each provider |
|
| `id` | 厂商唯一标识(8 位十六进制),在 Web 端添加时自动生成,手动配置时填写任意不重复的标识即可 |
|
||||||
| `name` | User-facing display label — can be freely renamed without breaking anything |
|
| `name` | 展示名称,可随意修改 |
|
||||||
| `model` | Optional; when set, this model is used instead of the global `model` field |
|
| `model` | 该厂商使用的模型,激活时生效 |
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
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])"`).
|
历史的单厂商配置(`bot_type` 为 `"custom"`,配合 `custom_api_key` / `custom_api_base`)仍然兼容,无需改动即可继续使用。
|
||||||
</Note>
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
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.
|
|
||||||
</Note>
|
</Note>
|
||||||
|
|||||||
Reference in New Issue
Block a user