mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-17 19:27:11 +08:00
Adds first-class support for configuring more than one custom OpenAI-compatible provider (e.g. SiliconFlow, DeepSeek, local vLLM) and switching the active one from the web console, addressing #2838. Backend: - config: new `custom_providers` (list) and `custom_active_provider` fields, fully backward compatible with the legacy single `open_ai_api_base`/`model` fields (used as fallback). - models/custom_provider.py: centralized resolver `resolve_custom_credentials()` returning (api_key, api_base, model), with active-provider selection and graceful fallback. - chat_gpt_bot.py wired to use the resolver. - web_channel.py: `_provider_overview` expands `custom_providers` into one card per provider (id `custom:<name>`, active flag, masked key); new POST actions `set_custom_provider`, `delete_custom_provider`, `set_active_custom_provider` with hermetic persistence + bridge reset. Frontend: - console.js: dedicated "Custom providers" section with add / edit / delete / set-active actions, masked-key keep-existing handling, and ~20 new zh/en i18n strings. - chat.html: custom provider modal. Tests: - tests/test_custom_provider.py (11) - resolver/config behavior. - tests/test_custom_provider_handlers.py (18) - write-side handlers and overview expansion, including duplicate-name rejection. All 29 unit tests pass.
85 lines
3.0 KiB
Python
85 lines
3.0 KiB
Python
# encoding:utf-8
|
|
|
|
"""
|
|
Centralized resolver for custom (OpenAI-compatible) provider credentials.
|
|
|
|
CowAgent historically supported only a *single* custom provider via the flat
|
|
config keys ``custom_api_key`` / ``custom_api_base``. This module adds support
|
|
for *multiple* custom providers (see issue #2838) while remaining 100%
|
|
backward compatible.
|
|
|
|
Config model
|
|
------------
|
|
- ``custom_providers``: list of dicts, each describing one custom provider::
|
|
|
|
{
|
|
"name": "siliconflow", # unique, user-facing identifier
|
|
"api_key": "sk-...", # required
|
|
"api_base": "https://...", # required, must be OpenAI-compatible
|
|
"model": "deepseek-ai/DeepSeek-V3" # optional default model
|
|
}
|
|
|
|
- ``custom_active_provider``: the ``name`` of the provider to use. When empty
|
|
(or pointing to a non-existent name) we fall back to the first provider in
|
|
the list, and finally to the legacy ``custom_api_key`` / ``custom_api_base``.
|
|
|
|
Backward-compatibility contract
|
|
-------------------------------
|
|
When ``custom_providers`` is empty, ``resolve_custom_credentials`` returns
|
|
exactly the legacy ``custom_api_key`` / ``custom_api_base`` values, so existing
|
|
deployments behave byte-for-byte identically.
|
|
"""
|
|
|
|
from config import conf
|
|
from common.log import logger
|
|
|
|
|
|
def get_custom_providers():
|
|
"""Return the list of configured custom providers (always a list)."""
|
|
providers = conf().get("custom_providers")
|
|
if not isinstance(providers, list):
|
|
return []
|
|
# Keep only well-formed entries with a name.
|
|
return [p for p in providers if isinstance(p, dict) and p.get("name")]
|
|
|
|
|
|
def _find_active_provider(providers):
|
|
"""Pick the active provider from the list, or None when list is empty."""
|
|
if not providers:
|
|
return None
|
|
active_name = conf().get("custom_active_provider") or ""
|
|
if active_name:
|
|
for p in providers:
|
|
if p.get("name") == active_name:
|
|
return p
|
|
logger.warning(
|
|
"[CUSTOM] active provider '%s' not found in custom_providers, "
|
|
"falling back to the first entry", active_name
|
|
)
|
|
return providers[0]
|
|
|
|
|
|
def resolve_custom_credentials():
|
|
"""Resolve the effective (api_key, api_base, model) for custom mode.
|
|
|
|
Resolution order:
|
|
1. The active entry in ``custom_providers`` (multi-provider mode).
|
|
2. The legacy flat keys ``custom_api_key`` / ``custom_api_base``.
|
|
|
|
:return: tuple ``(api_key, api_base, model)``. ``api_base`` and ``model``
|
|
may be ``None`` / empty when not configured.
|
|
"""
|
|
provider = _find_active_provider(get_custom_providers())
|
|
if provider is not None:
|
|
return (
|
|
provider.get("api_key", ""),
|
|
provider.get("api_base") or None,
|
|
provider.get("model") or None,
|
|
)
|
|
# Legacy single-provider fallback — unchanged behavior.
|
|
return (
|
|
conf().get("custom_api_key", ""),
|
|
conf().get("custom_api_base") or None,
|
|
None,
|
|
)
|