mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-19 12:47:25 +08:00
refactor(custom): id-based routing, single source of truth, security fixes
Rework the multi custom-provider design per maintainer review: 1. Data model: use server-generated uuid4 short id as primary key; 'name' is now a pure display label that can be freely renamed. 2. Routing: drop 'custom_active_provider'; activate a provider by setting bot_type to 'custom:<id>'. Single source of truth — no pointer drift between bot_type and a separate active selector. 3. Security: drag_sensitive() now recursively masks api_key/secret in nested structures (custom_providers list); previously only top-level string fields were masked. 4. Per-provider model: the provider's 'model' field now takes effect on the main chat path and agent path (was silently ignored before). 5. XSS fix: replace all inline onclick handlers in custom-provider UI with data-* attributes + event delegation. Provider names never appear in executable HTML contexts. Legacy compatibility: bot_type='custom' (no colon) still reads the flat custom_api_key/custom_api_base fields byte-for-byte identically. Closes: consolidates #2876 into this PR as requested. Ref: #2838
This commit is contained in:
@@ -4861,9 +4861,11 @@ function renderProviderLogo(p, sizePx) {
|
||||
// ---------- 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:<name>",
|
||||
// is_custom=true, custom_name and an `active` flag (see
|
||||
// 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() {
|
||||
return modelsState.providers.filter(isCustomProviderCard);
|
||||
@@ -4884,7 +4886,7 @@ function renderCustomProvidersSection() {
|
||||
<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 onclick="openCustomProviderModal('')"
|
||||
<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>
|
||||
@@ -4903,18 +4905,30 @@ function renderCustomProvidersSection() {
|
||||
}
|
||||
|
||||
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.
|
||||
// "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 onclick="setActiveCustomProvider('${nameEsc}')"
|
||||
: `<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>`;
|
||||
|
||||
@@ -4930,7 +4944,7 @@ function renderCustomProviderRow(p) {
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="flex items-center gap-3 px-3 py-2.5 rounded-lg border ${ring} transition-colors">
|
||||
<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">
|
||||
@@ -4939,11 +4953,11 @@ function renderCustomProviderRow(p) {
|
||||
</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 onclick="openCustomProviderModal('${nameEsc}')" title="${t('models_custom_edit_title')}"
|
||||
<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 onclick="deleteCustomProvider('${nameEsc}')" title="${t('models_custom_delete')}"
|
||||
<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>
|
||||
@@ -6245,16 +6259,15 @@ function clearVendorModal() {
|
||||
// =====================================================================
|
||||
// Custom (OpenAI-compatible) provider modal — add / edit
|
||||
// =====================================================================
|
||||
// State for the dedicated custom-provider modal. `originalName` is empty when
|
||||
// adding and set to the provider name when editing (so the backend can rename
|
||||
// without losing the entry).
|
||||
let customProviderModalState = { originalName: '' };
|
||||
// State for the dedicated custom-provider modal. `editId` is empty when
|
||||
// adding and set to the provider id when editing.
|
||||
let customProviderModalState = { editId: '' };
|
||||
|
||||
function openCustomProviderModal(name) {
|
||||
const editing = !!name;
|
||||
customProviderModalState = { originalName: editing ? name : '' };
|
||||
function openCustomProviderModal(providerId) {
|
||||
const editing = !!providerId;
|
||||
customProviderModalState = { editId: editing ? providerId : '' };
|
||||
|
||||
const card = editing ? getCustomProviderCards().find(p => p.custom_name === name) : null;
|
||||
const card = editing ? getCustomProviderCards().find(p => p.custom_id === providerId) : null;
|
||||
|
||||
const overlay = document.getElementById('custom-provider-modal-overlay');
|
||||
if (!overlay) return;
|
||||
@@ -6322,7 +6335,7 @@ function saveCustomProviderModal() {
|
||||
document.getElementById('custom-provider-name').focus();
|
||||
return;
|
||||
}
|
||||
const editing = !!customProviderModalState.originalName;
|
||||
const editing = !!customProviderModalState.editId;
|
||||
if (!editing && !apiBase) {
|
||||
showStatus('custom-provider-modal-status', 'models_custom_base_required', true);
|
||||
document.getElementById('custom-provider-base').focus();
|
||||
@@ -6342,7 +6355,7 @@ function saveCustomProviderModal() {
|
||||
model: model,
|
||||
};
|
||||
if (apiKey) payload.api_key = apiKey;
|
||||
if (editing) payload.original_name = customProviderModalState.originalName;
|
||||
if (editing) payload.id = customProviderModalState.editId;
|
||||
|
||||
const btn = document.getElementById('custom-provider-modal-save');
|
||||
btn.disabled = true;
|
||||
@@ -6356,10 +6369,7 @@ function saveCustomProviderModal() {
|
||||
closeCustomProviderModal();
|
||||
loadModelsView();
|
||||
} else {
|
||||
// Surface the most useful known error; fall back to generic save fail.
|
||||
const msg = (data.message || '').includes('already exists')
|
||||
? 'models_custom_name_exists' : 'models_save_failed';
|
||||
showStatus('custom-provider-modal-status', msg, true);
|
||||
showStatus('custom-provider-modal-status', 'models_save_failed', true);
|
||||
}
|
||||
}).catch(() => {
|
||||
btn.disabled = false;
|
||||
@@ -6367,17 +6377,17 @@ function saveCustomProviderModal() {
|
||||
});
|
||||
}
|
||||
|
||||
function setActiveCustomProvider(name) {
|
||||
function setActiveCustomProvider(providerId) {
|
||||
fetch('/api/models', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'set_active_custom_provider', name: name }),
|
||||
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(name) {
|
||||
function deleteCustomProvider(providerId) {
|
||||
showConfirmDialog({
|
||||
title: t('models_custom_delete_confirm_title'),
|
||||
message: t('models_custom_delete_confirm_msg'),
|
||||
@@ -6387,7 +6397,7 @@ function deleteCustomProvider(name) {
|
||||
fetch('/api/models', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'delete_custom_provider', name: name }),
|
||||
body: JSON.stringify({ action: 'delete_custom_provider', id: providerId }),
|
||||
}).then(r => r.json()).then(data => {
|
||||
if (data.status === 'success') loadModelsView();
|
||||
}).catch(() => { /* noop */ });
|
||||
|
||||
@@ -1601,7 +1601,7 @@ class ConfigHandler:
|
||||
"open_ai_api_key", "deepseek_api_key", "qianfan_api_key", "claude_api_key", "gemini_api_key",
|
||||
"zhipu_ai_api_key", "dashscope_api_key", "moonshot_api_key",
|
||||
"ark_api_key", "minimax_api_key", "linkai_api_key", "custom_api_key", "mimo_api_key",
|
||||
"custom_providers", "custom_active_provider",
|
||||
"custom_providers",
|
||||
"agent_max_context_tokens", "agent_max_context_turns", "agent_max_steps",
|
||||
"enable_thinking", "self_evolution_enabled", "web_password",
|
||||
}
|
||||
@@ -2137,10 +2137,9 @@ class ModelsHandler:
|
||||
"""Expand ``custom_providers`` into one card per provider.
|
||||
|
||||
Each user-defined OpenAI-compatible provider becomes its own card with
|
||||
a synthetic id ``custom:<name>`` so the frontend can render, edit,
|
||||
delete and activate them independently. The card carries
|
||||
``is_custom=True`` and ``active`` flags that the UI uses to render the
|
||||
extra controls (delete button, "set active" affordance).
|
||||
id ``custom:<id>`` so the frontend can render, edit, delete and
|
||||
activate them independently. The card carries ``is_custom=True`` and
|
||||
``active`` flags that the UI uses to render the extra controls.
|
||||
|
||||
Returns an empty list when no multi-providers are configured, in which
|
||||
case the caller keeps the single legacy ``custom`` card untouched —
|
||||
@@ -2148,7 +2147,7 @@ class ModelsHandler:
|
||||
``custom_api_key`` / ``custom_api_base`` config.
|
||||
"""
|
||||
try:
|
||||
from models.custom_provider import get_custom_providers
|
||||
from models.custom_provider import get_custom_providers, parse_custom_bot_type
|
||||
providers = get_custom_providers()
|
||||
except Exception as e: # pragma: no cover - defensive
|
||||
logger.warning(f"[ModelsHandler] failed to load custom_providers: {e}")
|
||||
@@ -2156,27 +2155,26 @@ class ModelsHandler:
|
||||
if not providers:
|
||||
return []
|
||||
|
||||
active_name = (local_config.get("custom_active_provider") or "").strip()
|
||||
# When no valid active name is set, the resolver treats the first entry
|
||||
# as active; mirror that here so exactly one card is highlighted.
|
||||
names = [p.get("name") for p in providers]
|
||||
if active_name not in names:
|
||||
active_name = names[0] if names else ""
|
||||
# Determine the currently active provider id from bot_type.
|
||||
bot_type = local_config.get("bot_type") or ""
|
||||
_, active_id = parse_custom_bot_type(bot_type)
|
||||
|
||||
meta = ConfigHandler.PROVIDER_MODELS.get("custom") or {}
|
||||
cards = []
|
||||
for p in providers:
|
||||
name = p.get("name") or ""
|
||||
pid = p.get("id") or ""
|
||||
name = p.get("name") or pid
|
||||
raw_key = p.get("api_key") or ""
|
||||
raw_base = p.get("api_base") or ""
|
||||
configured = cls._is_real_key(raw_key)
|
||||
cards.append({
|
||||
"id": f"custom:{name}",
|
||||
"id": f"custom:{pid}",
|
||||
"label": {"zh": name, "en": name},
|
||||
"configured": configured,
|
||||
"is_custom": True,
|
||||
"custom_id": pid,
|
||||
"custom_name": name,
|
||||
"active": (name == active_name),
|
||||
"active": (pid == active_id),
|
||||
"model": p.get("model") or "",
|
||||
# Custom cards are edited via the dedicated set_custom_provider
|
||||
# action, not the field-based set_provider flow, so the field
|
||||
@@ -2781,10 +2779,9 @@ class ModelsHandler:
|
||||
# ------------------------------------------------------------------
|
||||
# Multiple custom (OpenAI-compatible) providers
|
||||
# ------------------------------------------------------------------
|
||||
# These actions manage the ``custom_providers`` list and the
|
||||
# ``custom_active_provider`` selector. They are the write-side companion to
|
||||
# ``_custom_provider_cards`` and let the console add / edit / delete /
|
||||
# activate user-defined OpenAI-compatible providers individually.
|
||||
# These actions manage the ``custom_providers`` list. Activation is done
|
||||
# by setting ``bot_type`` to ``"custom:<id>"``. There is no separate
|
||||
# ``custom_active_provider`` field — a single source of truth.
|
||||
|
||||
@staticmethod
|
||||
def _normalize_custom_providers(raw) -> List[dict]:
|
||||
@@ -2793,20 +2790,22 @@ class ModelsHandler:
|
||||
return []
|
||||
out = []
|
||||
for p in raw:
|
||||
if isinstance(p, dict) and (p.get("name") or "").strip():
|
||||
if isinstance(p, dict) and (p.get("id") or "").strip():
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
def _persist_custom_providers(self, providers: List[dict], active_name) -> None:
|
||||
"""Write the providers list + active selector to both in-memory conf
|
||||
and the on-disk config, then reset the bridge so bots rebuild."""
|
||||
def _persist_custom_providers(self, providers: List[dict], bot_type=None) -> None:
|
||||
"""Write the providers list to both in-memory conf and the on-disk
|
||||
config, then reset the bridge so bots rebuild.
|
||||
|
||||
If ``bot_type`` is given, also update ``bot_type``."""
|
||||
local_config = conf()
|
||||
file_cfg = self._read_file_config()
|
||||
local_config["custom_providers"] = providers
|
||||
file_cfg["custom_providers"] = providers
|
||||
if active_name is not None:
|
||||
local_config["custom_active_provider"] = active_name
|
||||
file_cfg["custom_active_provider"] = active_name
|
||||
if bot_type is not None:
|
||||
local_config["bot_type"] = bot_type
|
||||
file_cfg["bot_type"] = bot_type
|
||||
self._write_file_config(file_cfg)
|
||||
self._reset_bridge()
|
||||
|
||||
@@ -2817,48 +2816,38 @@ class ModelsHandler:
|
||||
|
||||
{
|
||||
"action": "set_custom_provider",
|
||||
"name": "siliconflow", # required, unique
|
||||
"api_base": "https://...", # required when creating
|
||||
"api_key": "sk-...", # optional on edit (keep existing)
|
||||
"model": "deepseek-ai/...", # optional default model
|
||||
"original_name": "old-name", # optional, set when renaming
|
||||
"id": "3f2a9c1b", # required for edit; omit for create
|
||||
"name": "siliconflow", # required, display label
|
||||
"api_base": "https://...", # required when creating
|
||||
"api_key": "sk-...", # optional on edit (keep existing)
|
||||
"model": "deepseek-ai/...", # optional default model
|
||||
"make_active": true # optional, also activate it
|
||||
}
|
||||
"""
|
||||
from models.custom_provider import generate_provider_id, parse_custom_bot_type
|
||||
|
||||
name = (data.get("name") or "").strip()
|
||||
if not name:
|
||||
return json.dumps({"status": "error", "message": "name is required"})
|
||||
|
||||
provider_id = (data.get("id") or "").strip()
|
||||
api_base = (data.get("api_base") or "").strip()
|
||||
# api_key omitted/empty on edit => keep the existing one.
|
||||
api_key_raw = data.get("api_key")
|
||||
api_key = api_key_raw.strip() if isinstance(api_key_raw, str) else ""
|
||||
model = (data.get("model") or "").strip()
|
||||
# ``original_name`` is supplied only when editing an existing entry
|
||||
# (so it can be renamed). Its absence means "create a new provider";
|
||||
# we must keep that distinction explicit, otherwise a create request
|
||||
# for an already-taken name would be misread as an in-place edit.
|
||||
original_name = (data.get("original_name") or "").strip()
|
||||
is_edit = bool(original_name)
|
||||
make_active = bool(data.get("make_active"))
|
||||
|
||||
local_config = conf()
|
||||
providers = self._normalize_custom_providers(local_config.get("custom_providers"))
|
||||
|
||||
# Reject a name collision unless it is the very entry being edited.
|
||||
for p in providers:
|
||||
if p.get("name") == name and p.get("name") != original_name:
|
||||
return json.dumps({
|
||||
"status": "error",
|
||||
"message": f"a custom provider named {name!r} already exists",
|
||||
})
|
||||
|
||||
existing = next((p for p in providers if p.get("name") == original_name), None) if is_edit else None
|
||||
existing = next((p for p in providers if p.get("id") == provider_id), None) if provider_id else None
|
||||
if existing is None:
|
||||
# Creating a new provider — api_base is mandatory.
|
||||
if not api_base:
|
||||
return json.dumps({"status": "error", "message": "api_base is required"})
|
||||
entry = {"name": name, "api_key": api_key, "api_base": api_base}
|
||||
provider_id = generate_provider_id()
|
||||
entry = {"id": provider_id, "name": name, "api_key": api_key, "api_base": api_base}
|
||||
if model:
|
||||
entry["model"] = model
|
||||
providers.append(entry)
|
||||
@@ -2876,64 +2865,68 @@ class ModelsHandler:
|
||||
existing.pop("model", None)
|
||||
created = False
|
||||
|
||||
# Decide the active selector.
|
||||
active_name = (local_config.get("custom_active_provider") or "").strip()
|
||||
if make_active or created and not active_name:
|
||||
# Decide bot_type.
|
||||
_, current_active_id = parse_custom_bot_type(local_config.get("bot_type") or "")
|
||||
new_bot_type = None
|
||||
if make_active or (created and not current_active_id):
|
||||
# Activate on explicit request, or auto-activate the very first
|
||||
# provider so the resolver has a definite target.
|
||||
active_name = name
|
||||
elif active_name == original_name and original_name != name:
|
||||
# The active provider was renamed; keep it pointed at the new name.
|
||||
active_name = name
|
||||
new_bot_type = f"custom:{provider_id}"
|
||||
|
||||
self._persist_custom_providers(providers, active_name)
|
||||
self._persist_custom_providers(providers, new_bot_type)
|
||||
logger.info(
|
||||
f"[ModelsHandler] custom provider {name!r} "
|
||||
f"{'created' if created else 'updated'} (active={active_name!r})"
|
||||
f"[ModelsHandler] custom provider {name!r} (id={provider_id}) "
|
||||
f"{'created' if created else 'updated'}"
|
||||
)
|
||||
return json.dumps({
|
||||
"status": "success",
|
||||
"id": provider_id,
|
||||
"name": name,
|
||||
"created": created,
|
||||
"active": active_name,
|
||||
})
|
||||
|
||||
def _handle_delete_custom_provider(self, data: dict) -> str:
|
||||
"""Remove a custom provider by name."""
|
||||
name = (data.get("name") or "").strip()
|
||||
if not name:
|
||||
return json.dumps({"status": "error", "message": "name is required"})
|
||||
"""Remove a custom provider by id."""
|
||||
from models.custom_provider import parse_custom_bot_type
|
||||
|
||||
provider_id = (data.get("id") or "").strip()
|
||||
if not provider_id:
|
||||
return json.dumps({"status": "error", "message": "id is required"})
|
||||
|
||||
local_config = conf()
|
||||
providers = self._normalize_custom_providers(local_config.get("custom_providers"))
|
||||
remaining = [p for p in providers if p.get("name") != name]
|
||||
remaining = [p for p in providers if p.get("id") != provider_id]
|
||||
if len(remaining) == len(providers):
|
||||
return json.dumps({"status": "error", "message": f"unknown custom provider: {name}"})
|
||||
return json.dumps({"status": "error", "message": f"unknown custom provider id: {provider_id}"})
|
||||
|
||||
active_name = (local_config.get("custom_active_provider") or "").strip()
|
||||
if active_name == name:
|
||||
# The active provider was removed — fall back to the first
|
||||
# remaining entry (resolver does the same when the name is stale).
|
||||
active_name = remaining[0]["name"] if remaining else ""
|
||||
# If the deleted provider was active, fall back to the first remaining.
|
||||
_, current_active_id = parse_custom_bot_type(local_config.get("bot_type") or "")
|
||||
new_bot_type = None
|
||||
if current_active_id == provider_id:
|
||||
if remaining:
|
||||
new_bot_type = f"custom:{remaining[0]['id']}"
|
||||
else:
|
||||
new_bot_type = "custom" # revert to legacy
|
||||
|
||||
self._persist_custom_providers(remaining, active_name)
|
||||
logger.info(f"[ModelsHandler] custom provider {name!r} deleted (active={active_name!r})")
|
||||
return json.dumps({"status": "success", "name": name, "active": active_name})
|
||||
self._persist_custom_providers(remaining, new_bot_type)
|
||||
logger.info(f"[ModelsHandler] custom provider id={provider_id} deleted")
|
||||
return json.dumps({"status": "success", "id": provider_id})
|
||||
|
||||
def _handle_set_active_custom_provider(self, data: dict) -> str:
|
||||
"""Mark one of the existing custom providers as active."""
|
||||
name = (data.get("name") or "").strip()
|
||||
if not name:
|
||||
return json.dumps({"status": "error", "message": "name is required"})
|
||||
"""Activate a custom provider by setting bot_type to 'custom:<id>'."""
|
||||
provider_id = (data.get("id") or "").strip()
|
||||
if not provider_id:
|
||||
return json.dumps({"status": "error", "message": "id is required"})
|
||||
|
||||
local_config = conf()
|
||||
providers = self._normalize_custom_providers(local_config.get("custom_providers"))
|
||||
if not any(p.get("name") == name for p in providers):
|
||||
return json.dumps({"status": "error", "message": f"unknown custom provider: {name}"})
|
||||
if not any(p.get("id") == provider_id for p in providers):
|
||||
return json.dumps({"status": "error", "message": f"unknown custom provider id: {provider_id}"})
|
||||
|
||||
self._persist_custom_providers(providers, name)
|
||||
logger.info(f"[ModelsHandler] active custom provider set to {name!r}")
|
||||
return json.dumps({"status": "success", "active": name})
|
||||
new_bot_type = f"custom:{provider_id}"
|
||||
self._persist_custom_providers(providers, new_bot_type)
|
||||
logger.info(f"[ModelsHandler] active custom provider set to id={provider_id}")
|
||||
return json.dumps({"status": "success", "active_id": provider_id})
|
||||
|
||||
def _handle_set_capability(self, data: dict) -> str:
|
||||
capability = (data.get("capability") or "").strip()
|
||||
|
||||
Reference in New Issue
Block a user