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:
zhayujie
2026-06-12 18:03:33 +08:00
parent 63bfab03f6
commit 6b5ee245ae
15 changed files with 234 additions and 400 deletions

View File

@@ -1643,6 +1643,32 @@ class ConfigHandler:
"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 "")
masked_pwd = ("*" * len(raw_pwd)) if raw_pwd else ""
@@ -2189,6 +2215,17 @@ class ModelsHandler:
})
return cards
@classmethod
def _legacy_custom_in_use(cls, local_config: dict) -> bool:
"""True when the flat single-provider custom config is still relevant:
either it is the active bot_type, or its key/base fields are filled.
In that case the legacy "custom" card must stay visible even when
multi ``custom_providers`` entries exist."""
if (local_config.get("bot_type") or "") == "custom":
return True
return (cls._is_real_key(local_config.get("custom_api_key") or "")
or bool(local_config.get("custom_api_base")))
@classmethod
def _provider_overview(cls) -> List[dict]:
"""All known providers (configured first, unconfigured after).
@@ -2202,13 +2239,18 @@ class ModelsHandler:
"""
local_config = conf()
custom_cards = cls._custom_provider_cards(local_config)
# Keep the legacy single "custom" card visible alongside the expanded
# ones when the flat custom_api_key/base config is active or filled,
# so existing single-provider setups never disappear from the UI.
keep_legacy_custom = cls._legacy_custom_in_use(local_config)
items = []
for pid, p in ConfigHandler.PROVIDER_MODELS.items():
if pid == "custom" and custom_cards:
# Multi-provider mode: emit the expanded cards instead of the
# single legacy custom card.
# Multi-provider mode: emit the expanded cards, plus the
# legacy card when it is still in use.
items.extend(custom_cards)
continue
if not keep_legacy_custom:
continue
key_field = p.get("api_key_field")
base_field = p.get("api_base_key")
raw_key = local_config.get(key_field, "") if key_field else ""
@@ -2253,11 +2295,15 @@ class ModelsHandler:
provider_id = "linkai"
# In multi-provider mode, replace the single "custom" entry with the
# expanded "custom:<id>" ids so the chat dropdown matches the cards.
# The legacy "custom" entry stays when its flat config is still used.
provider_ids = []
custom_cards = cls._custom_provider_cards(local_config)
keep_legacy_custom = cls._legacy_custom_in_use(local_config)
for pid in ConfigHandler.PROVIDER_MODELS.keys():
if pid == "custom" and custom_cards:
provider_ids.extend(c["id"] for c in custom_cards)
if keep_legacy_custom:
provider_ids.append(pid)
else:
provider_ids.append(pid)
return {
@@ -2881,11 +2927,14 @@ class ModelsHandler:
existing["api_base"] = api_base
if api_key:
existing["api_key"] = api_key
# model is always overwritten (empty clears the default model).
if model:
existing["model"] = model
else:
existing.pop("model", None)
# Only touch model when explicitly provided in the payload; an
# explicit empty string clears it, a missing key keeps it (the
# UI modal no longer sends model, so manual config survives edits).
if "model" in data:
if model:
existing["model"] = model
else:
existing.pop("model", None)
created = False
# Decide bot_type — only switch when explicitly requested.